Many programs have to respond to time in one form or another. Some programs measure time—they time things. An example of this type of program is one that monitors the time it takes other programs (or parts of programs) to run. Other programs are
controlled by time—they exist to respond to "time events" external to the program itself. An example of this type of program is a scheduler.
As the remainder of this chapter demonstrates, Visual Basic 4.0 has facilities to easily create both types of programs without resorting to external C functions or Windows SDK calls.
The ability to measure time is an important part of any programming language. For example, games depend heavily on timing to pace the action so that they don't run too fast. Even business applications use delays to pace the application. In the technical
arena, the ability to measure time is what makes code profilers possible.
Fortunately, Visual Basic provides a handy way to measure time: the built-in Timer function. The operation of the Timer function is simple: it returns the number of seconds since midnight. Although this is an arbitrary number and is not very useful to
look at, it provides a way to mark a point in time. For example, if you call Timer, store the value it returns, and later call the function again, the difference between the two values is the elapsed time in seconds between the two calls. This simple
formula is the basis for all timing done with the Timer function.
Probably the most simple, and common, use for Timer is when you need to insert a timed delay into your program. Back in the old days of GWBASIC, delays were often inserted into programs with empty FOR...NEXT loops like the following one:
FOR I = 1 TO 1000 NEXT I
This method is actually a poor way to insert delays. Its main flaw is that the actual amount of time it delays the program is unpredictable because it depends totally on the speed of the computer it is running on. In addition, you cannot use this method
to set a specific delay even when you know which machine you are using unless you do a lot of tinkering to get the number of iterations to match the desired delay. You are likely to have to play with the number of iterations several times and may end with
code like FOR I = 1 TO 1127 to set a delay of one second—at least on your machine.
With the Timer function, your delays can be a little more precise. A simple example of a Timer-based function that delays a program for a number of seconds is as follows:
Sub Sleep(sngNumberOfSeconds As Single) Dim sngEndTime As Single ' compute the time until the end of the delay sngEndTime = Timer + sngNumberOfSeconds 'Spin until the time is up Do Loop Until Timer >= sngEndTime End Sub
This subroutine does what was advertised—you call it and it comes back the specified number of seconds later. But it has some problems. If you run this subroutine in the 16-bit versions of Windows (Windows 3.x), you will notice that nothing else in
the system responds during the delay loop. This is caused by the lack of a DoEvents function call inside the Do...Loop. Without a DoEvents call, nothing else in Windows can happen because that function is the heart of the cooperative multitasking
architecture of 16-bit Windows. Cooperative multitasking depends on applications to be "nice" and release control so that other applications can have some processor time. The DoEvents function is the way Visual Basic applications can fulfill this
requirement to be "nice" to other applications. Note that releasing time to other applications is automatic when you are just waiting for user input; however, when you are running in a tight loop, like the preceding subroutine, DoEvents is
required. The revised Sleep subroutine would look like this:
Sub Sleep(sngNumberOfSeconds As Single) Dim sngEndTime As Single ' compute the time until the end of the delay sngEndTime = Timer + sngNumberOfSeconds ' Spin until the time is up Do DoEvents ' Let other things happen Loop Until Timer >= sngEndTime End Sub
Even in the 32-bit environments like Windows NT or Windows 95, the DoEvents function serves a purpose. Although you don't have the problem of locking up other applications by not releasing time (both Windows NT and Windows 95 preemptively multitask
32-bit applications), your applications do not receive or process any events while you are in a tight loop. Use DoEvents to allow your application to respond to events during the delay.
The next problem with the simple Sleep subroutine shown earlier is something the documentation and the help system don't really make clear: the Timer function resets at midnight. If you begin timing at 11:59 p.m. and want to wait for 180 seconds (3
minutes), the Sleep subroutine never returns. Although this isn't obvious, it becomes evident when you walk through the code:
In fact, any delay that goes past midnight suffers from this problem. This bug becomes even more problematic if you forget to call DoEvents in the loop: your application (and even the entire system under Windows 3.x) stops responding during that endless
loop.
The problem of timing past midnight is easily solved with a little extra code. Following is a revised Sleep subroutine that shows how to account for the passing of midnight:
Sub Sleep(sngNumberOfSeconds As Single) Const SECONDS_IN_DAY = 86400! Dim sngStartTime As Single, sngEndTime As Single, sngCurrentTime As Single ' setup the timer start and end times sngStartTime = Timer sngEndTime = sngStartTime + sngNumberOfSeconds ' spin until the time has elapsed Do DoEvents ' if the current time is less than the start time, then we know that ' we've passed midnight. If that happens, apply an adjustment ' so the values will compare correctly. sngCurrentTime = Timer If sngCurrentTime < sngStartTime Then sngCurrentTime = sngCurrentTime + SECONDS_IN_DAY End If Loop Until sngCurrentTime >= sngEndTime End Sub
The preceding Sleep subroutine is more complex than the simple, but flawed, one shown earlier. However, the subroutine is readily understandable when you look at it in detail:
Note that during the loop, the subroutine references Timer only once, storing its value in a variable for later use. Why? Besides some minor speed improvements, the subroutine has to store the Timer value because the value returned by Timer may change
between the different calls. By storing the value in a program variable, you can ensure that the value is always consistent during the subroutine's computations and comparisons.
In the Sleep subroutine used in the preceding section, notice that all Timer-related variables are declared as single-precision numbers. Why is that, when Timer simply returns the number of seconds since midnight? This is another fact that the
documentation doesn't clearly bring out: the Timer function actually returns a fractional number of seconds. By using single-precision numbers to compute the delay, you can use the Timer function to time a fractional number of seconds. Because Sleep uses
single-precision variables, all the following calls to Sleep are valid:
Sleep 10 Sleep .5 Sleep 2.78
You can demonstrate for yourself that Timer returns a fractional number of seconds by using the small program shown in Figure 4.1. The example is simple—click anywhere on the form, and the program prints the next 10 values returned by the Timer
function as fast as it can. Most of the values printed are the same because the loop runs fast enough to print several Timer values before Timer increments. But you should see the Timer function increment in a few of those values.
Figure 4.1. Demonstrating Timer resolution.
Take note of a curious thing: the Timer values increment in steps of .03 or .04 instead of .01. This increment is the granularity of the Timer function (that is, the smallest amount of time Timer can measure). Keep this granularity in mind when you
write programs based on the Timer function; the increment may not be as fine as you expect.
As you saw in the first part of this chapter, an important use of the Timer function is to insert delays in a program. However, a delay is simply a piece of code that continually measures the time elapsed from its start to the desired end time. Using
similar concepts, you can use the Timer function to time things other than a simple delay loop. The process is conceptually very simple: record the value returned by Timer, perform the process you want to time, and record a second Timer value. The
difference between the two values is the number of seconds that elapsed while the process was being performed.
The two calls to Timer can be easily encapsulated into a pair of function calls—one to start the timer and another to calculate the difference and return it. With a little enhancement on the basic concept, you can create a useful program profiler,
like the one shown in Listing 4.1.
Option Explicit #If PROFILING Then Const MAX_SECONDS = 86400 ' number of seconds at midnight Dim iFileHandle As Integer ' handle for the output file Dim sngTimers(20) As Single ' storage for 20 simultaneous timers Dim iLastTimer As Integer ' last entry in the array used #End If Function StartProfiler(sFileName As String) As Integer #If PROFILING Then On Error GoTo Error_StartProfiler ' open an output file for the timing information iFileHandle = FreeFile Open sFileName For Output As #iFileHandle ' initialize the counters iLastTimer = 0 StartProfiler = True Exit Function Error_StartProfiler: StartProfiler = False Exit Function #Else StartProfiler = True Exit Function #End If End Function Function StopProfiler() As Integer #If PROFILING Then On Error GoTo Error_StopProfiler ' close the output file. Close #iFileHandle StopProfiler = True Exit Function Error_StopProfiler: StopProfiler = False Exit Function #Else StopProfiler = True Exit Function #End If End Function Function MarkStartTime() As Integer #If PROFILING Then On Error GoTo Error_MarkStartTime Dim i As Integer ' get the next entry to use i = iLastTimer + 1 ' record the starting time value sngTimers(i) = Timer iLastTimer = i MarkStartTime = True Exit Function Error_MarkStartTime: MarkStartTime = False Exit Function #Else MarkStartTime = True Exit Function #End If End Function Function MarkEndTime(sTag As String) As Integer #If PROFILING Then On Error GoTo Error_MarkEndTime Dim sngEndTime As Single, sngDifference As Single ' record the end time and adjust for the passage of midnight sngEndTime = Timer If sngEndTime < sngTimers(iLastTimer) Then sngEndTime = sngEndTime + _MAX_SECONDS ' compute the elapsed time sngDifference = sngEndTime - sngTimers(iLastTimer) ' write it to a file, with a number of spaces that reflect ' its place in the stack of timers. Print #iFileHandle, Spc(iLastTimer); sTag; " "; sngDifference ' free an array entry for re-use iLastTimer = iLastTimer - 1 MarkEndTime = True Exit Function Error_MarkEndTime: MarkEndTime = False Exit Function #Else MarkEndTime = True Exit Function #End If End Function
The profiler example in Listing 4.1 works on a stack concept—multiple timers can be started, but they must be stopped in the reverse of their starting order. The reason you use multiple timers is simply so that you can time at multiple levels: You
can measure the time it takes for an entire process to run and also measure the time individual portions of that process consume. The functions also record the timing information to a program-specified file rather than to any kind of window so that the
functions can be inserted and have no impact on the actual user interface of the program (except for whatever speed impact they may add).
The profiling functions are globally enabled or disabled using Visual Basic 4.0's conditional compilation options. By setting the PROFILING compilation constant to a nonzero value, you enable the profiling code. When the profiling is disabled (PROFILING
is either not defined or is defined as zero), all the functions return TRUE immediately and do nothing else. By immediately returning when they are not enabled, the profiling functions should have minimal impact on your program's speed. This arrangement
means that you can insert the profiling functions in your application and simply disable them when you do the production compile, rather than going though your code to remove them manually.
To add the profiler to your project, include the PROFILE.BAS module and set PROFILING=1 in the Conditional Compilation Arguments field in the Tools | Options window under the Advanced tab. Then simply insert the calls to the profiling functions in the
sections of code where you want timing information, as the example in Figure 4.2 shows.
Figure 4.2. Using the profiling functions.
To use the profiling functions, insert the StartTimer function in your code before the first time you want to begin making profiling calls. All StartTimer does is to open the specified output file (which holds the profiling information) and to set the
counter for the last timer, iLastTimer, to zero so that you begin fresh in the array. The StopTimer function is a cleanup function that closes the timing output file and returns.
The MarkStartTime and MarkEndTime functions are the ones that actually do the timing. MarkStartTime allocates a timer value from the sngTimers array and sets it to the current value of the Timer function. MarkEndTime simply computes the elapsed time
based on the last entry in the sngTimers array and writes the time difference to a file, along with any programmer-specified text passed into the function. MarkEndTime then removes the used timer from the array by decrementing iLastTimer.
When you run your program, the profiling information is silently written to the file you specified so that you can examine it at your leisure.
There is an inherent problem with routines based on the Timer function: because Timer resets every 24 hours, you can't use it to time anything greater than 24 hours. Also, although the values returned by Timer are fine for computing the raw number of
seconds a process takes, these values force the programmer to convert by hand if information is required in minutes or seconds. If these limitations are unacceptable for your purposes, consider building your timers based on the Now function.
Now returns a Date type instead of a simple number of seconds. The good news is that, because it is a Date type, you can time things for any number of days (or months and years). The bad news is that, because it is a Date type, doing computations and
displaying the timing information is a little more complex: you have to use the special Visual Basic date functions.
The easiest way to get the difference between two Date values is to use the DateDiff function. With DateDiff, you can compute the difference between two Date variables in a unit of time you specify. As with the Timer function, the difference between
these two variables is the elapsed time. The following is a version of the Sleep subroutine shown earlier in this chapter, but this time it uses the Now and DateDiff functions:
Sub Sleep(lNumberOfSeconds As Long) Dim dateStart As Date ' record the start time dateStart = Now() 'Spin until the time is up Do DoEvents Loop Until DateDiff("s", dateStart, Now()) >= lNumberOfSeconds End Sub
The preceding loop is much simpler than the Sleep subroutine used earlier in this chapter—it needs no adjustment for passing midnight. And because the example provides an S as the first parameter to the DateDiff function, the function simply
returns the number of seconds between the two dates—even if the number of seconds gets ridiculously large!
There are some drawbacks to this implementation:
Based on these limitations, a Timer-based solution is probably the better choice unless you need to time some very long events.
People are mostly time driven. We wake up, go to work, go home, and go to bed at regular times. In other words, we respond to time-based events. Some programs must function the same way to be useful. A scheduler program is a classic example of this type
of program; something as simple as an animated picture in a screen saver has similar requirements. That is, they both must do something at a predetermined time or interval. Fortunately, Windows provides a set of built-in timers you can use to create this
type of program.
Windows timers can be thought of as an old-fashioned grandfather clock. This type of clock chimes at regular intervals, in most cases on the hour and half-hour. Windows timers work similarly, except that you can set the "chime" interval to any
value you want, and instead of an audible bell tone, you get a "Timer Event." To access built-in Windows timers in Visual Basic code, simply place a Timer control on your form.
You place a Timer control on a form in the same way as any other control: select the Timer control icon from the toolbar and place it on a form. Don't worry about the size and placement of the control; the Timer control automatically assumes a default
size and becomes invisible at runtime. Figure 4.3 shows the Timer control in the toolbar and on a form.
Figure 4.3. Using the Timer control.
Timer controls have very few properties compared to other Visual Basic controls. The following chart gives a rundown on the two most important ones:
Property |
Description |
Enabled |
Essentially turns the timer on and off. Set this property to TRUE if you want to receive Timer events (that is, if you want to start the timer); set this to FALSE if you don't. |
|
|
The Top and Left properties have no bearing on the operation of the Timer control because the control is be invisible at runtime and can't receive any mouse or keyboard events. The best way to position a Timer control is to move it to some unused part
of the form and leave it (its position really doesn't matter and you might as well have it out of the way).
A Timer control has only one event associated with it: the Timer event. This event is triggered at each passing of the specified Interval, if the Timer control is Enabled. By placing code in the Timer event subroutine of the Timer control, you can cause
something to happen on each Interval.
One of the most basic uses of a timer is to add simple animation to a window. A common business use of animation is to get an operator's or user's attention during the run of a long process. Listing 4.2 uses the Timer control to flash the border of a
window using the Windows SDK FlashWindow function.
Option Explicit ' declarations differ between Windows 3.1 and Win32. #If Win16 Then Private Declare Function FlashWindow Lib "User" (ByVal hWnd As Integer, ByVal _bInvert As Integer) As Integer #Else Private Declare Function FlashWindow Lib "User32" (ByVal hWnd As Long, ByVal _bInvert As Long) As Long #End If Private Sub cbFlash_Click() timerFlasher.Interval = 500 timerFlasher.Enabled = True End Sub Private Sub cbStop_Click() Dim iRet As Integer ' stop the flashing timerFlasher.Enabled = False ' Restore the window to its proper state iRet = FlashWindow(Me.hWnd, False) End Sub Private Sub Form_Load() ' make sure the timer isn't running when the program loads. timerFlasher.Enabled = False End Sub Private Sub timerFlasher_Timer() Dim iRet As Integer ' make the window flash iRet = FlashWindow(Me.hWnd, True) End Sub
The FlashWindow function simply toggles the specified window's title bar and border between the Active and Inactive colors specified in the Control Panel. The hWnd parameter is the internal Windows handle for the window to be flashed. You can retrieve
this value from the hWnd property on the Visual Basic Form object. When the next parameter is TRUE, it tells FlashWindow to toggle the window's title and border color, either from Active to Inactive or vice versa. If this parameter is FALSE, it returns the
window to whatever color is appropriate for its current state—Active if the window is active or Inactive if the window is not active.
You'll notice the Conditional Compilation #IF...THEN that is around the declaration for the FlashWindow function. In fact, there are two declarations for FlashWindow: one that references the User library and one that references User32. This section
illustrates a point about programming for multiple platforms. The first declaration of the FlashWindow function is for Windows 3.x—the 16-bit version of Windows. The second is for the 32-bit Windows versions—Windows 95 and Windows NT. You need
two different declarations because Windows needs two versions of that module: one for 16-bit applications and one for 32-bit. Thus, Microsoft named them differently to prevent confusion.
Another difference between the two declarations is in the types of the parameters passed to and returned from FlashWindow. The parameters for the 32-bit versions are now Long variables instead of Integers. This is because when a machine is running a
32-bit operating system, its word size becomes a 32-bit value. Thus, the parameters are now Longs instead of Integers because an Integer is only 16-bits.
The Click event for the cbFlash button turns on the Timer control and sets the proper Interval for the flash (in this case, 500 milliseconds, or 1/2 second). The Click event for the cbStop button first shuts off the Timer and then returns the window to
its proper state.
You can see that having the window flash on a Timer event doesn't keep the window from responding to other events, such as the click of a button. This kind of window is ideal for operator notification in a computer room, especially when the computer's
monitor may be sitting in a row with several others. To be even more attention grabbing (or annoying), place a call to the Beep function in the Timer event.
A few caveats about the Timer control:
Figure 4.4 shows the variability in the regularity of the Timer event.
Figure 4.4. Demonstrating Timer variability.
All the example program in Figure 4.4 does is record the elapsed time between Timer events using the Timer function. When you run this program, it prints the time in seconds that elapsed between the scheduled Timer events.
Even when the system is idling, events don't fall exactly on the scheduled time. Try running another program while looking at this display. Notice that more than the desired Interval time may pass between Timer events. This can cause some complications,
especially if you are writing a program that depends on very regular Intervals. If that is a requirement, your only alternative is to time with a loop based on the Timer function.
Another common use for Timer controls is to schedule applications. Such scheduler programs wait silently until there is something to do. Usually, these programs wake up at a particular time, but some look for other things such as the existence of a
file.
The simplest version of this process is shown in the following example:
Do Until iDone DoEvents ' let other events process IsThereAnythingToDo ' check to see if there is anything to do Loop
This function works but it has some drawbacks. It is a real time waster. This loop always spins, regardless of whether or not there is anything to do—even if it just checked half a millisecond ago. Although the DoEvents function ensures that other
things can happen while this loop is spinning, all the bookkeeping and event checking will be a drag to all other processes in the system.
It usually isn't necessary to check continuously to see whether there is something to do. Checking at a longer interval, perhaps once a second or once a minute, is usually fine. Following is a revised version of the preceding scheduler loop that checks
for new events only once every second:
Do Until iDone DoEvents ' let other events process IsThereAnythingToDo ' check to see if there is anything to do Sleep 1 ' snooze for a second Loop
This example doesn't check to see whether there is anything to do quite so often and is less of a drag on the system's performance. However, the Sleep subroutine still has a loop that spins, with all the bookkeeping overhead involved. Other operating
systems, such as UNIX, have a built-in Sleep function that places a program in a special "suspended" state for a period of time. While asleep, the operating system gives the program very little processor time until the specified duration has
elapsed. Windows has no such function. The Timer control comes the closest to providing this capability: your application doesn't have to have any code running to check the time; all it has to do is sit back and wait for the Timer event to occur.
The implementation of a scheduler with the Timer control is a little different than the two non-Timer control implementations just shown. Instead of a loop, add code in the Timer event of the Timer control on the form, like this:
Private Sub timerScheduler_Timer() IsThereAnythingToDo ' check to see if there is anything to do End Sub
In the preceding example, there is no loop, no Sleep to delay, and no DoEvents call. Your program doesn't have to loop because the code in this event is executed on each passing of the specified Interval. Your program also doesn't have to delay itself
to pace the number of checks it performs because the Timer control goes back to sleep when you return from this subroutine. Finally, your program doesn't need a call to DoEvents because, when you exit this subroutine, you automatically return control to
Windows.
With the addition of a little code to build a list of things to schedule, you can have a basic scheduling program, like the one shown in Listing 4.3. Figure 4.5 shows the controls on the programs form.
Figure 4.5. The form for the scheduler example in Listing 4.3.
Option Explicit Const SCHEDULE_DATA_FILE = "schedule.dat" Private Type SCHEDULE_ITEM_TYPE ' set up a structure to hold the schedule _entries. dteTime As Date sCommand As String End Type Dim strItems() As SCHEDULE_ITEM_TYPE ' items in the schedule Dim iCount As Integer Dim dteLastTimeRun As Date Private Sub cbClose_Click() End ' end this program End Sub Private Sub Form_Load() If Not ReadScheduledItems() Then ' fill the schedule array End End If dteLastTimeRun = Time timerScheduler.Enabled = True ' start the timer End Sub Private Sub timerScheduler_Timer() PrintTime ' Print the current time on the window IsThereAnythingToDo ' Check the scheduled list End Sub Private Function ReadScheduledItems() As Integer Dim sFileName As String Dim iFile As Integer On Error GoTo Error_ReadScheduledItems ' Open the schedule file, assume that it is in the app.path sFileName = App.Path & "\" & SCHEDULE_DATA_FILE iFile = FreeFile Open sFileName For Input As #iFile iCount = 0 Do While Not EOF(iFile) iCount = iCount + 1 ' expand the structure to hold the additional item ReDim Preserve strItems(iCount) ' read it into the structure Input #iFile, strItems(iCount).dteTime, strItems(iCount).sCommand Loop Close #iFile ReadScheduledItems = True Exit Function Error_ReadScheduledItems: ReadScheduledItems = False Exit Function End Function Private Sub IsThereAnythingToDo() Dim i As Integer, iRet As Integer Dim dteCurrentTime As Date On Error Resume Next dteCurrentTime = Time ' run through the array of things to do For i = 1 To iCount ' If the scheduled start time is between the time of the last run ' and the current time, then it is time to start the specified ' program. If strItems(i).dteTime > dteLastTimeRun And strItems(i).dteTime <= _dteCurrentTime Then iRet = Shell(strItems(i).sCommand, 1) ' run the program If Err <> 0 Then MsgBox "Error running: " & strItems(i).sCommand End If End If Next dteLastTimeRun = dteCurrentTime End Sub Private Sub PrintTime() Dim sTime As String ' only print the time if the the window is not minimized If WindowState = 0 Then Me.Cls sTime = Format$(Time, "Long Time") ' format the time Me.CurrentX = (Me.ScaleWidth - Me.TextWidth(sTime)) / 2 ' center the time Me.CurrentY = Me.TextHeight(sTime) / 2 Me.Print sTime ' and print it. End If End Sub
This example is a basic scheduler that reads a list of scheduled commands from a file and executes them at the appointed time. The user interface is simple; the program prints the current time on each Timer event and provides only a Close button that
ends the program.
When the form loads, the program reads the items to be run from the file using the ReadScheduledItems function; it then starts the timer by enabling the Timer control. The only purpose for the cbClose_Click() subroutine is to force the program to end.
The format of the file that contains the items to be scheduled is also very simple—it was designed to be read quickly by Visual Basic's Input statement. Each line describes a schedule entry. The first parameter for each line is the time at which
the command should be executed; the second parameter is a string that contains the command to be executed. An example of this file is given here:
#09:15:00 pm#, "calc.exe" #09:17:00 pm#, "notepad.exe" #09:20:00 pm#, "wordpad.exe"
The memory management in the ReadScheduledItems subroutine is not the best example of how to read information into a dynamic array. Calling ReDim for each element being read is slow and can cause fragmentation in the pool of memory used by Visual Basic.
A better solution is to expand the array in fixed-sized extents, perhaps 10 at a time. This arrangement prevents you from going back to Windows for more memory on each iteration and eliminates a lot of the moving of data that goes on with the ReDim
Preserve statement.
The heart of the scheduling engine is in the Timer event subroutine, timerScheduler_Timer(). This subroutine simply prints the current time on the form using the PrintTime subroutine and then calls the IsThereAnythingToDo subroutine, which actually runs
any scheduled event whose time has come:
Determining when it is time to run an item is more complex than it seems. You can't simply use the following logic to compare to see whether the desired runtime is greater than the current time:
If Time > strItems(i).dteTime Then
The problem with this test is that the condition is true not only at the time the item is to be run, but also every time after that, until midnight. The command in question runs at the appointed time and at every timer interval thereafter!
The simplest way to determine when a program must be run is to see whether the desired runtime has passed between the last Timer event and the current one. To do this, store the time of the last Timer event so that you know the lower bound of the time
span between the clicks. To see whether a program must be run, simply see whether it falls between the last Timer event and the current one:
If strItems(i).dteTime > dteLastTimeRun And strItems(i).dteTime <= dteCurrentTime Then
The basic formula for determining whether or not an item must be run is LastTimeRun > DesiredRunTime >= CurrentTime. Note that one of the comparison operators in the preceding If statement is a "less than or equal to" operator (<=).
This operator is used to accommodate the odd occasion on which a Timer event falls exactly on the scheduled time for an item. If you don't use this operator, events that fall exactly on the time of the Timer event are not executed.
Also note that you must store the real time (using the Timer function) of both the last Timer event and the current one; you cannot simply assume that just the specified Interval has passed. This arrangement compensates for the fact that Timer events
don't always come at exactly the specified intervals, as demonstrated earlier in this chapter.
In addition to sharing the same name, the Timer function and the Timer control also have a lot of overlap in their functionality. They both deal with time, and to some degree, can be used to replace each other.
As described earlier in this chapter, delay loops based on the Timer function can be used to schedule things. They aren't always the best solution, however, because they require a part of your program to always be spinning while waiting for the next
scheduled time. But these delay loops are serviceable and don't depend on getting a Timer event from Windows. They are also reasonably accurate (as accurate as anything is in a multitasking operating system).
What may not be obvious is that the Timer control can be used to help in measuring time. By nature, the Timer control is not very good at fine time measurements (remember that it depends on your program being in a state to accept messages to work), but
it is good for measuring gross time between parts of an application.
An example of a common use of a Timer control to measure time is in some of the popular disk backup programs that measure the total time the backup took; these programs also split out the amount of time the program took to actually perform the backup
versus the time it spent waiting for the user. The following code fragment demonstrates one way to implement this type of timer measurement:
Dim sngLastTime As Single ' the last time we record the time, for bookkeeping Dim sngUserTime As Single ' a bucket to hold the time spent waiting for user response Dim sngProgramTime As Single ' a bucket to hold the time our program has spent running Private Sub Timer1_Timer() Dim sngElapsedTime As Single, sngCurrentTime As Single sngCurrentTime = Timer ' decide where to allocate this time If iWaitingOnUser Then sngUserTime = sngCurrentTime - sngLastTime Else sngProgramTime = sngCurrentTime - sngLastTime End If PrintTimes sngLastTime = sngCurrentTime End Sub
All this code does is add the elapsed time since the last Timer event to either user time or program time, based on the iWaitingOnUser flag. By simply flipping the iWaitingOnUser flag, you can allocate time to both measurements without adding a lot of
code in the application's main logic.
Note that you still use the Timer function to actually measure the elapsed time. Again, this is because we can't depend on the Timer events to match exactly (or even come close to) the desired intervals.
The ability to measure and respond to time is an important part of any programming environment. Although it is not a very good choice for rigorous real-time applications, Visual Basic 4.0 provides some handy and easy-to-use tools for time management.
To measure time, Visual Basic 4.0 provides the Timer function to measure time in raw seconds. The Timer function is the basis for nearly all time measurement in Visual Basic. When you have to measure larger amounts of time—even potentially a number
of days or weeks—Visual Basic provides the Now function.
To respond to time, Visual Basic 4.0 provides the Timer control, which lets your Visual Basic applications tap into the power of Window's timer capability and write your own animated or scheduling programs.